/*******************************************************************************
 Jimm - Mobile Messaging - J2ME ICQ clone
 Copyright (C) 2003-06  Jimm Project

 This program is free software; you can redistribute it and/or
 modify it under the terms of the GNU General Public License
 as published by the Free Software Foundation; either version 2
 of the License, or (at your option) any later version.

 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with this program; if not, write to the Free Software
 Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 ********************************************************************************
 File: src/jimm/comm/ActionListener.java
 Version: ###VERSION###  Date: ###DATE###
 Author(s): Manuel Linsmayer, Andreas Rossbacher, Spassky Alexander, Igor Palkin
 *******************************************************************************/

// #sijapp cond.if protocols_ICQ is "true" #
package protocol.icq;

import jimm.*;
import jimm.chat.message.*;
import jimm.comm.*;
import protocol.*;
import protocol.icq.action.*;
import protocol.icq.packet.*;
import protocol.icq.plugin.*;
import jimm.modules.*;
import jimm.util.JLocale;

public final class IcqNetDefActions {
    public static final int FLAG_HAPPY    = 0x0008;
    public static final int FLAG_WEBAWARE = 0x0001;

    private IcqNetWorking connection;
    public IcqNetDefActions(IcqNetWorking net) {
        connection = net;
    }
    private Icq getIcq() {
        return connection.getIcq();
    }

    private void sendPacket(Packet packet) throws JimmException {
        connection.sendPacket(packet);
    }
    
    /** ************************************************************************* */
    private void processOfflineMessage(byte[] buf) {
        // Check length
        if (buf.length < 14) return;
        
        // Extract UIN
        long uinRaw = Util.getDWordLE(buf, 0);
        
        String uin = String.valueOf(uinRaw);
        
        // Get text length
        int textLen = Util.getWordLE(buf, 12);
        
        String text = null;
        // Check length
        if (buf.length >= 14 + textLen) {
            text = StringConvertor.byteArrayToString(buf, 14, textLen);
        }
        if (StringConvertor.isEmpty(text)) {
            return;
        }

        // Extract date of dispatch
        long date = Util.gmtTimeToLocalTime(Util.createLongTime(
                Util.getWordLE(buf, 4),
                Util.getByte(buf, 6),
                Util.getByte(buf, 7),
                Util.getByte(buf, 8), Util.getByte(buf, 9), 0)) + 4 * 3600L;
        
        // Get type
        int type = Util.getWordLE(buf, 10);
        
        
        if (0x0001 == type) { // Normal message
            addOfflineMessage(uin, text, date);
        }
    }
    // Forwards received packet
    public void forward(Packet packet) throws JimmException {
        // Watch out for channel 4 (Disconnect) packets
        if (packet instanceof DisconnectPacket) {
            DisconnectPacket disconnectPacket = (DisconnectPacket) packet;

            // Throw exception
            throw disconnectPacket.makeException();
        }

        if (packet instanceof FromIcqSrvPacket) {
            FromIcqSrvPacket fromIcqSrvPacket = (FromIcqSrvPacket) packet;
            
            // Watch out for SRV_OFFLINEMSG
            if (FromIcqSrvPacket.SRV_OFFLINEMSG_SUBCMD == fromIcqSrvPacket.getSubcommand()) {
                processOfflineMessage(fromIcqSrvPacket.getData());
                
                // Watch out for SRV_DONEOFFLINEMSGS
            } else if (FromIcqSrvPacket.SRV_DONEOFFLINEMSGS_SUBCMD == fromIcqSrvPacket.getSubcommand()) {
                sendPacket(new ToIcqSrvPacket(0x00000000, getIcq().getUserId(), 
                        ToIcqSrvPacket.CLI_ACKOFFLINEMSGS_SUBCMD, new byte[0], new byte[0]));

            // #sijapp cond.if modules_DEBUGLOG is "true" #
            } else  {
                unknownPacket(packet);
            // #sijapp cond.end#
            }

        } else if (packet instanceof SnacPacket) {
            SnacPacket snacPacket = (SnacPacket) packet;
            int family = snacPacket.getFamily();
            int command = snacPacket.getCommand();

            if (SnacPacket.CONTACT_FAMILY == family) {
                if (SnacPacket.SRV_USERONLINE_COMMAND == command) {
                    userOnline(snacPacket);
                
                } else if (SnacPacket.SRV_USEROFFLINE_COMMAND == command) {
                    userOffline(snacPacket);

                } else if (0x1F == command) {
                    // #sijapp cond.if modules_MAGIC_EYE is "true" #
                    MagicEye.addAction(getIcq(), "1", "Verification");
                    // #sijapp cond.end #
                    // #sijapp cond.if modules_DEBUGLOG is "true" #
                    DebugLog.println("Verification");
                    // #sijapp cond.end #
                    
                // #sijapp cond.if modules_DEBUGLOG is "true" #
                } else  {
                    unknownPacket(packet);
                // #sijapp cond.end#
                }

            } else if (SnacPacket.CLI_ICBM_FAMILY == family) {
                if (SnacPacket.CLI_ACKMSG_COMMAND == command) {
                    ackMessage(snacPacket.getData());
                    
                } else if (SnacPacket.SRV_RECVMSG_COMMAND == command) {
                    try {
                        readMessage(snacPacket.getData());
                    } catch (Exception e) {
                    }
                
                // #sijapp cond.if modules_DEBUGLOG is "true" #
                } else if (SnacPacket.SRV_MISSED_MESSAGE_COMMAND == command) {
                    byte[] data = snacPacket.getData();
                    int uinLen = data[2];
                    String uin = StringConvertor.byteArrayToWinString(data, 3, uinLen);
                    int marker = 5 + uinLen;
                    int tlvCount = Util.getWordBE(data, marker);
                    marker += 2;
                    for (int i = 0; i < tlvCount; ++i) {
                        byte[] tlvData = Util.getTlv(data, marker);
                        marker += 4 + tlvData.length;
                    }
                    int count = Util.getWordBE(data, marker);
                    int reasone = Util.getWordBE(data, marker + 2);
                    DebugLog.println("msg: " + uin + " [" + reasone +  "]x("+count+")");
                // #sijapp cond.end#
	
                // #sijapp cond.if modules_SOUND is "true" #
                } else if (SnacPacket.SRV_MTN_COMMAND == command) {
                    // Typing notify
                    byte[] p = snacPacket.getData();
                    int uin_len = Util.getByte(p, 10);
                    String uin = StringConvertor.byteArrayToWinString(p, 11, uin_len);
                    int flag = Util.getWordBE(p, 11 + uin_len);
                    if (Options.getInt(Options.OPTION_TYPING_MODE) > 0) {
                        getIcq().beginTyping(uin, (0x0002 == flag));
                    }
                // #sijapp cond.end#

                // #sijapp cond.if modules_DEBUGLOG is "true" #
                } else {
                    unknownPacket(packet);
                // #sijapp cond.end#
                }
                
                //	  Watch out for SRV_ADDEDYOU
            } else if (SnacPacket.SSI_FAMILY == family) {
                if (SnacPacket.SRV_ADDEDYOU_COMMAND == command) {
                    // Get data
                    byte[] buf = snacPacket.getData();
                    
                    // Get UIN of the contact changing status
                    int uinLen = Util.getByte(buf, 0);
                    String uin = StringConvertor.byteArrayToWinString(buf, 1, uinLen);
                    
                    // Create a new system notice
                    SystemNotice notice = new SystemNotice(getIcq(), SystemNotice.SYS_NOTICE_YOUWEREADDED, uin, null);
                    
                    // Handle the new system notice
                    getIcq().addMessage(notice);
                    
                    //	  Watch out for SRV_GRAND
                } else if (SnacPacket.SRV_GRANT_FUTURE_AUTH_COMMAND == command) {
                    int authMarker = 0;
                    
                    // Get data
                    byte[] buf = snacPacket.getData();
                    
                    // Get UIN of the contact changing status
                    int length = Util.getByte(buf, 0);
                    authMarker += 1;
                    String uin = StringConvertor.byteArrayToWinString(buf, authMarker, length);
                    authMarker += length;
                    
                    String reason = getReasone(buf, authMarker);
                    
                    getIcq().setAuthResult(uin, true);
                    getIcq().addMessage(new SystemNotice(getIcq(), SystemNotice.SYS_NOTICE_GRANDED, uin, reason));
                    
                    //	  Watch out for SRV_AUTHREQ
                } else if (SnacPacket.SRV_AUTHREQ_COMMAND == command) {
                    int authMarker = 0;
                    
                    // Get data
                    byte[] buf = snacPacket.getData();
                    
                    // Get UIN of the contact changing status
                    int length = Util.getByte(buf, 0);
                    authMarker += 1;
                    String uin = StringConvertor.byteArrayToWinString(buf, authMarker, length);
                    authMarker += length;
                    
                    String reason = getReasone(buf, authMarker);
                    
                    getIcq().addMessage(new SystemNotice(getIcq(), SystemNotice.SYS_NOTICE_AUTHREQ, uin, reason));
                    
                    //	  Watch out for SRV_AUTHREPLY
                } else if (SnacPacket.SRV_AUTHREPLY_COMMAND == command) {
                    
                    int authMarker = 0;
                    // Get data
                    byte[] buf = snacPacket.getData();
                    
                    // Get UIN of the contact changing status
                    int length = Util.getByte(buf, 0);
                    authMarker += 1;
                    String uin = StringConvertor.byteArrayToWinString(buf, authMarker, length);
                    authMarker += length;
                    
                    // Get granted boolean
                    boolean granted = (0x01 == Util.getByte(buf, authMarker));
                    authMarker += 1;
                    
                    String reason = getReasone(buf, authMarker);

                    // Handle the new system notice
                    if (granted) {
                        getIcq().setAuthResult(uin, true);
                    } else {
                        getIcq().addMessage(new SystemNotice(getIcq(), SystemNotice.SYS_NOTICE_AUTHDENY, uin, reason));
                    }

                } else if (SnacPacket.CLI_ROSTERDELETE_COMMAND == command) {
                    byte[] buf = snacPacket.getData();
                    int length = Util.getWordBE(buf, 0);
                    String uin = StringConvertor.byteArrayToWinString(buf, 2, length);

                    int type = Util.getWordBE(buf, length + 6);
                    if (0 == type) {
                        Contact c = getIcq().getItemByUIN(uin);
                        if ((null != c) && !c.isTemp()) {
                            c.setTempFlag(true);
                            String message = JLocale.getString("contact_has_been_removed");
                            if (c.hasChat()) {
                                getIcq().addMessage(new SystemNotice(getIcq(),
                                        SystemNotice.SYS_NOTICE_MESSAGE, uin, message));
                            }
                            // #sijapp cond.if modules_MAGIC_EYE is "true" #
                            MagicEye.addAction(getIcq(), uin, message);
                            // #sijapp cond.end #
                        }
                    }

                // #sijapp cond.if modules_DEBUGLOG is "true" #
                } else  {
                    unknownPacket(packet);
                // #sijapp cond.end#
                }

            // #sijapp cond.if modules_DEBUGLOG is "true" #
            } else  {
                unknownPacket(packet);
            // #sijapp cond.end#
            }
        }
    }
    private String getReasone(byte[] buf, int offset) {
        String reason = null;
        try {
            int length = Util.getWordBE(buf, offset);
            offset += 2;
            reason = StringConvertor.byteArrayToString(buf, offset, length);
        } catch (Exception e) {
        }
        return reason;
    }

    // Merge two received capabilities into one byte array
    private byte[] mergeCapabilities(byte[] oldCaps, byte[] newCaps) {
        if (null == newCaps) {
            return oldCaps;
        }
        
        // Extend new capabilities to match with old ones
        int newCapsCount = 0;
        for (int i = 0; i < newCaps.length; i += 2) {
            if (0x13 != newCaps[i]) continue;
            newCapsCount++;
        }
        if (0 == newCapsCount) {
            return oldCaps;
        }
        final int oldCapsSize = (null == oldCaps) ? 0 : oldCaps.length;
        
        final byte[] allCaps = new byte[newCapsCount * 16 + oldCapsSize];
        int allCapsPos = 0;

        if (0 < oldCapsSize) {
            System.arraycopy(oldCaps, 0, allCaps, allCapsPos, oldCapsSize);
            allCapsPos += oldCapsSize;
        }

        final byte[] CAP_OLD = GUID.CAP_DC.toByteArray();
        for (int i = 0; i < newCaps.length; i += 2) {
            if (0x13 != newCaps[i]) continue;
            System.arraycopy(CAP_OLD, 0, allCaps, allCapsPos, CAP_OLD.length);
            System.arraycopy(newCaps, i, allCaps, allCapsPos + 2, 2);
            allCapsPos += 16;
        }
        
        return allCaps;
    }
        
    // #sijapp cond.if modules_DEBUGLOG is "true" #
    private void unknownPacket(Packet packet) {
        if (!(packet instanceof SnacPacket)) {
            return;
        }
        SnacPacket snacPacket = (SnacPacket) packet;
        int family = snacPacket.getFamily();
        int command = snacPacket.getCommand();
        if (0x1 == family) {
            if (0x21 == command) return;// my iMessage
            if (0x0F == command) return;// my status info
//            if (0x0A == command) {
//                DebugLog.dump("0x01 0x0A", snacPacket.getData());
//            }
        }
        if (packet instanceof FromIcqSrvPacket) {
            FromIcqSrvPacket fisp = (FromIcqSrvPacket)packet;
            byte[] d = fisp.getData();
            int cmd = Util.getWordLE(d, 0);
            DebugLog.println("pkt: family 0x" + Integer.toHexString(family)
                    + " command 0x" + Integer.toHexString(command)
                    + " type 0x" + Integer.toHexString(fisp.getSubcommand())
                    + " subtype 0x" + Integer.toHexString(cmd)
                    + " length="  + snacPacket.getData().length);
        } else {
            DebugLog.println("pkt: family 0x" + Integer.toHexString(family)
                    + " command 0x" + Integer.toHexString(command)
                    + " length="  + snacPacket.getData().length);
        }
    }
    // #sijapp cond.end#

    private void ackMessage(byte[] buf) {
        // Get length of the uin (needed for skipping)
        int uinLen = Util.getByte(buf, 10);
        // Get message type
        if ((58 + uinLen) < buf.length) {
            int msgType = Util.getWordLE(buf, 58 + uinLen);
            int textLen = Util.getWordLE(buf, 64 + uinLen);
            String uin = StringConvertor.byteArrayToWinString(buf, 11, uinLen);
            if ((SendMessageAction.MESSAGE_TYPE_AWAY <= msgType)
                    && (msgType <= SendMessageAction.MESSAGE_TYPE_FFC)) {
                IcqContact item = (IcqContact)getIcq().getItemByUIN(uin);
                if (null != item) {
                    //String statMsg = StringConvertor.byteArrayToString(buf, 66 + uinLen, textLen);
                    String statMsg = StringConvertor.byteArrayToWinString(buf, 66 + uinLen, textLen);
                    item.setStatusMessage(statMsg);
                }
                
            } else if (msgType == SendMessageAction.MESSAGE_TYPE_EXTENDED) {
                XtrazMessagePlugin plg = unpackPlugin(buf, 66 + uinLen + textLen, uin);
                if (null != plg) {
                    plg.activate(getIcq());
                }
            }
        }
    }
    private String removeHtml(String text) {
        if (!text.startsWith("<HTML>")) {
            return text;
        }
        StringBuffer escapedText = new StringBuffer();
        boolean inTag = false;
        for (int i = 0; i < text.length(); ++i) {
            char ch = text.charAt(i);
            if (inTag) {
                if ('>' == ch) {
                    inTag = false;
                }

            } else {
                if ('<' == ch) {
                    inTag = true;
                    if (text.startsWith("br", i + 1) || text.startsWith("BR", i + 1)) {
                        escapedText.append("\n");
                    }
                } else {
                    escapedText.append(ch);
                }
            }
        }
        return Util.xmlUnescape(escapedText.toString());
    }

    private boolean isMrim(String userId) {
        return -1 != userId.indexOf('@');
    }
    private void addMessage(String uin, String text) {
        if (StringConvertor.isEmpty(text)) {
            return;
        }
        if (isMrim(uin)) {
            // FIXME: it's hack
            text = Util.xmlUnescape(text);
            text = StringConvertor.convert(StringConvertor.MRIM2JIMM, text);
        }
        text = removeHtml(text);
        text = StringConvertor.removeCr(text);
        text = StringConvertor.trim(text);
        if (StringConvertor.isEmpty(text)) {
            return;
        }
        getIcq().addMessage(new PlainMessage(uin, getIcq(), Util.createCurrentDate(false), text, false));
    }
    private void addOfflineMessage(String uin, String text, long date) {
        if (StringConvertor.isEmpty(text)) {
            return;
        }
        text = StringConvertor.removeCr(text);
        text = StringConvertor.trim(text);
        if (StringConvertor.isEmpty(text)) {
            return;
        }
        getIcq().addMessage(new PlainMessage(uin, getIcq(), date, text, true));
    }
    
    void sendRecivedFlag(byte[] buf, String uin, byte[] msg2Buf) throws JimmException {
        // Acknowledge message
        Util packet = new Util();
        packet.writeByteArray(buf, 0, 8);
        packet.writeWordBE((null == msg2Buf) ? 0x0002 : 0x0002);
        packet.writeShortLenAndUtf8String(uin);
        packet.writeWordBE(0x0003);
        
        if (null != msg2Buf) {
            packet.writeByteArray(msg2Buf, 0, 51);
            packet.writeWordLE(0x0001);
            packet.writeByte(0x00);
        } else {
            packet.writeWordLE(0x001b);
            packet.writeWordLE(0x0008);
            packet.writeDWordLE(0);
            packet.writeDWordLE(0);
            packet.writeDWordLE(0);
            packet.writeDWordLE(0);
            packet.writeWordLE(0x0000);
            packet.writeDWordLE(0x00000003);
            packet.writeByte(0x00);
            packet.writeWordLE(0xFFFE);
            packet.writeWordLE(0x000E);
            packet.writeWordLE(0xFFFE);
            packet.writeDWordLE(0);
            packet.writeDWordLE(0);
            packet.writeDWordLE(0);
            packet.writeDWordLE(1);
            packet.writeWordLE(1);

            packet.writeWordLE(0x0001);
            packet.writeByte(0x00);
            // cannel 1
            //packet.writeDWordLE(0x00000105);
            //packet.writeDWordLE(0x04000101);
            //packet.writeDWordLE(0x00000000);
        }
        //DebugLog.dump("sendRecivedFlag", packet.toByteArray());
        sendPacket(new SnacPacket(SnacPacket.CLI_ICBM_FAMILY,
                SnacPacket.CLI_ACKMSG_COMMAND, 0, packet.toByteArray()));
    }
    private void readMessage(byte[] buf) throws JimmException {
        int marker = 0;
        
        // Get message format
        marker += 8;
        int format = Util.getWordBE(buf, marker);
        marker += 2;
        
        // Get UIN length
        int uinLen = Util.getByte(buf, marker);
        marker += 1;
        
        // Get UIN
        String uin = StringConvertor.byteArrayToWinString(buf, marker, uinLen);
        marker += uinLen;
        
        // Skip WARNING
        marker += 2;
        
        int tlvType;
        // Skip TLVS
        int tlvCount = Util.getWordBE(buf, marker);
        marker += 2;
        for (int i = 0; i < tlvCount; ++i) {
            byte[] tlvData = Util.getTlv(buf, marker);
            marker += 4 + tlvData.length;
        }
        
        // Get message data and initialize marker
        byte[] msgBuf;
        do {
            msgBuf = Util.getTlv(buf, marker);
            tlvType = Util.getWordBE(buf, marker);
            marker += 4 + msgBuf.length;
        } while ((tlvType != 0x0002) && (tlvType != 0x0005));
        int msgMarker = 0;
        
        //////////////////////
        // Message format 1 //
        //////////////////////
        if (format == 0x0001) {
            
            // Variables for all possible TLVs
            // byte[] capabilities = null;
            byte[] message = null;
            
            // Read all TLVs
            while (msgMarker < msgBuf.length) {
                
                // Get next TLV
                byte[] tlvValue = Util.getTlv(msgBuf, msgMarker);                
                // Get type of next TLV
                tlvType = Util.getWordBE(msgBuf, msgMarker);
                
                // Update markers
                msgMarker += 4 + tlvValue.length;
                
                // Save value
                switch (tlvType) {
                    case 0x0501:
                        // capabilities
                        // capabilities = tlvValue;
                        break;
                    case 0x0101:
                        // message
                        message = tlvValue;
                        break;
                }
                
            }
            
            // Process packet if at least the message TLV was present
            if (null != message) {
                
                boolean ucs2 = (0x0002 == Util.getWordBE(message, 0));
                // Get message text
                String text = null;
                if (ucs2) {
                    text = StringConvertor.ucs2beByteArrayToString(message, 4, message.length - 4);
                } else {
                    text = StringConvertor.byteArrayToWinString(message, 4, message.length - 4);
                }
                
                // Construct object which encapsulates the received
                // plain message
                addMessage(uin, text);
                sendRecivedFlag(buf, uin, null);
            }
            
            //////////////////////
            // Message format 2 //
            //////////////////////
        } else if (format == 0x0002) {
            // TLV(A): Acktype 0x0000 - normal message
            //                 0x0001 - file request / abort request
            //                 0x0002 - file ack
            
            // Get and validate SUB_MSG_TYPE2.COMMAND
            int cmd = Util.getWordBE(msgBuf, msgMarker);
            if (0x0000 != cmd) return; // Only normal messages are
            // supported yet
            msgMarker += 2;
            
            // Skip SUB_MSG_TYPE2.TIME and SUB_MSG_TYPE2.ID
            msgMarker += 4 + 4;
            // Skip SUB_MSG_TYPE2.CAPABILITY
            msgMarker += 16;
            
            // #sijapp cond.if modules_FILES is "true"#
            int ackType = -1;
            byte[] extIP = new byte[4];
            byte[] ip = new byte[4];
            int port = 0;
            int status = -1;
            // #sijapp cond.end#

            // Get message data and initialize marker
            byte[] msg2Buf = null;
            do {
                byte[] infoBuf = Util.getTlv(msgBuf, msgMarker);
                tlvType = Util.getWordBE(msgBuf, msgMarker);
                // #sijapp cond.if modules_FILES is "true"#
                switch (tlvType) {
                    case 0x0003: System.arraycopy(infoBuf, 0, extIP, 0, 4);  break;
                    case 0x0004: System.arraycopy(infoBuf, 0, ip, 0, 4);     break;
                    case 0x0005: port = Util.getWordBE(infoBuf, 0);         break;
                    case 0x000a: ackType = Util.getWordBE(infoBuf, 0);      break;
                }
                // #sijapp cond.end#
                if (0x2711 == tlvType) {
                    msg2Buf = infoBuf;
                }
                msgMarker += 4 + infoBuf.length;
            } while (msgMarker < msgBuf.length);

            // Get message data and initialize marker
            if (null == msg2Buf) {
                return;
            }
            int msg2Marker = 0;
            
            // Skip values up to (and including) SUB_MSG_TYPE2.UNKNOWN
            // (before MSGTYPE)
            msg2Marker += Util.getWordLE(msg2Buf, msg2Marker) + 2;
            msg2Marker += Util.getWordLE(msg2Buf, msg2Marker) + 2;
//            msg2Marker += 2 + 2 + 16 + 3 + 4 + 2 + 2 + 2 + 12;
            
            // Get and validate message type
            int msgType = Util.getWordLE(msg2Buf, msg2Marker);
            msg2Marker += 2;
            if (!((SendMessageAction.MESSAGE_TYPE_PLAIN == msgType)
                    || (SendMessageAction.MESSAGE_TYPE_URL == msgType)
                    || (SendMessageAction.MESSAGE_TYPE_PLUGIN == msgType)
                    || ((msgType >= 1000) && (msgType <= 1004)))) {
                // #sijapp cond.if modules_DEBUGLOG is "true" #
                if (SendMessageAction.MESSAGE_TYPE_ADDED == msgType) {
                    DebugLog.println("msg type 0x" + Integer.toHexString(msgType));
                }
                // #sijapp cond.end #
                return;
            }
            
            // #sijapp cond.if modules_FILES is "true"#
            status = Util.getWordBE(msg2Buf, msg2Marker);
            // #sijapp cond.end#
            msg2Marker += 2;
            
            // Skip PRIORITY
            msg2Marker += 2;
            
            // Get length of text
            int textLen = Util.getWordLE(msg2Buf, msg2Marker);
            msg2Marker += 2;
            // Get raw text
            byte[] rawText = new byte[textLen];
            System.arraycopy(msg2Buf, msg2Marker, rawText, 0, textLen);
            msg2Marker += textLen;

            // Plain message or URL message
            if (0x0001 == msgType) {
                if (rawText.length == 0) {
                    return;
                }
                
                // Skip FOREGROUND and BACKGROUND
                msg2Marker += 4 + 4;
                
                // Check encoding (by checking GUID)
                boolean isUtf8 = false;
                if (msg2Buf.length >= msg2Marker + 4) {
                    int guidLen = (int) Util.getDWordLE(msg2Buf, msg2Marker);
                    if (guidLen == 38) {
                        String guid = StringConvertor.byteArrayToWinString(msg2Buf, msg2Marker + 4, guidLen);
                        if ("{0946134E-4C7F-11D1-8222-444553540000}".equals(guid)) {
                            isUtf8 = true;
                        }
                    }
                    msg2Marker += 4 + guidLen;
                }
                
                // Decode text and create Message object
                String text = null;
                // Decode text
                if (isUtf8) {
                    text = StringConvertor.utf8beByteArrayToString(rawText, 0, rawText.length);
                } else {
                    //TODO: log it
                    text = StringConvertor.byteArrayToWinString(rawText);
                }
                // Forward message object to contact list
                addMessage(uin, text);
                sendRecivedFlag(buf, uin, msg2Buf);

            } else if (0x0004 == msgType) { // URL
                
                // Extended message
            } else if (msgType == 0x001A) {
                
                XtrazMessagePlugin plg = unpackPlugin(msg2Buf, msg2Marker, uin);
                if (null != plg) {
                    plg.setCookie(Util.getDWordBE(buf, 0), Util.getDWordBE(buf, 4));
                    plg.activate(getIcq());
                    return;
                }
                
                // Status message requests
            } else if ((1000 <= msgType) && (msgType <= 1004)) {
                // #sijapp cond.if modules_MAGIC_EYE is "true" #
                MagicEye.addAction(getIcq(), uin, "read status message");
                // #sijapp cond.end #
                IcqContact to = (IcqContact)getIcq().getItemByUIN(uin);
                if ((null == to) || !getIcq().isMeVisible(to)) return;

                String statusMess = getIcq().getProfile().xstatusTitle;
                if (StringConvertor.isEmpty(statusMess)) {
                    statusMess = "";
                } else {
                    statusMess += " " + getIcq().getProfile().xstatusDescription;
                    statusMess = statusMess.trim();
                }

                // Acknowledge message with away message
                final byte[] statusMessBytes = StringConvertor.stringToByteArray(statusMess);
                
                
                Util packet = new Util();
                packet.writeByteArray(buf, 0, 10);
                packet.writeShortLenAndUtf8String(uin);
                packet.writeWordBE(0x0003);
                packet.writeByteArray(msg2Buf, 0, 2);
                packet.writeWordBE(0x0800);
                packet.writeByteArray(msg2Buf, 4, 51 - 4);
                packet.writeWordLE(statusMessBytes.length + 1);
                packet.writeByteArray(statusMessBytes);
                packet.writeByte(0x00);
                sendPacket(new SnacPacket(SnacPacket.CLI_ICBM_FAMILY,
                        SnacPacket.CLI_ACKMSG_COMMAND, 0, packet.toByteArray()));
            }
            
            //////////////////////
            // Message format 4 //
            //////////////////////
        } else if (format == 0x0004) {
            
            // Skip SUB_MSG_TYPE4.UIN
            msgMarker += 4;
            
            // Get SUB_MSG_TYPE4.MSGTYPE
            int msgType = Util.getWordLE(msgBuf, msgMarker);
            msgMarker += 2;
            
            // Only plain messages and URL messagesa are supported
            if (msgType != 0x0001) return;
            
            // Get length of text
            int textLen = Util.getWordLE(msgBuf, msgMarker);
            msgMarker += 2;
            
            // Get text
            //String text = StringConvertor.byteArrayToString(msgBuf, msgMarker, textLen);
            String text = StringConvertor.byteArrayToWinString(msgBuf, msgMarker, textLen);
            msgMarker += textLen;
            
            // Forward message to contact list
            addMessage(uin, text);
        }
    }
    
    private String lastOfflineUin = null;
    private void userOffline(SnacPacket snacPacket) {
        // Get raw data
        byte[] buf = snacPacket.getData();
        
        // Get UIN of the contact that goes offline
        int uinLen = Util.getByte(buf, 0);
        String uin = StringConvertor.byteArrayToWinString(buf, 1, uinLen);
        
        IcqContact item = (IcqContact)getIcq().getItemByUIN(uin);
        if (null != item) {
            boolean hasBeenOffline = !item.isOnline();
            // #sijapp cond.if modules_MAGIC_EYE is "true" #
            if (getIcq().isConnected() && hasBeenOffline) {
                if (item.getUserId().equals(lastOfflineUin)) {
                    MagicEye.addAction(getIcq(), lastOfflineUin, "hiding_from_you");
                }
                lastOfflineUin = item.getUserId();
            }
            // #sijapp cond.end#
            if (hasBeenOffline) {
                return;
            }
            
            item.setOfflineStatus();
            // Update contact list
            getIcq().ui_changeContactStatus(item);
            
            // #sijapp cond.if modules_DEBUGLOG is "true" #
        } else {
            DebugLog.println("USER_OFFLINE for " + uin);
            // #sijapp cond.end#
        }
    }
    private void userOnline(SnacPacket snacPacket) {
        // DC variables
        byte[] internalIP = new byte[4];
        byte[] externalIP = new byte[4];
        int dcPort = 0;
        int dcType = -1;
        int authCookie = 0;
        int icqProt = 0;
        
        int dwFT1 = 0;
        int dwFT2 = 0;
        int dwFT3 = 0;
        byte[] capabilities_old = new byte[0]; // Buffer for old style capabilities (TLV 0x000D)
        byte[] capabilities_new = null; // Buffer for new style capabilities (TLV 0x0019)
        String statusText = null;
        String mood = null;
        
        // Time variables
        int idle = -1;
        int online = -1;
        long signon = -1;
        
        // Get data
        byte[] buf = snacPacket.getData();
        
        // Get UIN of the contact changing status
        int uinLen = Util.getByte(buf, 0);
        String uin = StringConvertor.byteArrayToWinString(buf, 1, uinLen);
        // Get new status and client capabilities
        int status = IcqStatusInfo.STATUS_ONLINE;
        int marker = 1 + uinLen + 2;
        int tlvNum = Util.getWordBE(buf, marker);
        marker += 2;
        for (int i = 0; i < tlvNum; ++i) {
            int tlvType = Util.getWordBE(buf, marker);
            byte[] tlvData = Util.getTlv(buf, marker);

            if (tlvType == 0x0006) {// STATUS
                status = (int)Util.getDWordBE(tlvData, 0);
            } else if (tlvType == 0x000D) {// Old style CAPABILITIES
                capabilities_old = tlvData;
            } else if (tlvType == 0x0019) {// New style CAPABILITIES
                capabilities_new = tlvData;
            } else if (tlvType == 0x000A) {// External IP
                System.arraycopy(tlvData, 0, externalIP, 0 ,4);
            } else if (tlvType == 0x000C) {// DC Infos
                // dcMarker
                int dcMarker = 0;
                
                // Get internal IP
                System.arraycopy(tlvData, dcMarker, internalIP, 0, 4);
                dcMarker += 4;
                
                // Get tcp port
                dcPort = (int)Util.getDWordBE(tlvData, dcMarker);
                dcMarker += 4;
                
                // Get DC type
                dcType = Util.getByte(tlvData, dcMarker);
                dcMarker ++;
                
                // Get protocol version
                icqProt = Util.getWordBE(tlvData, dcMarker);
                dcMarker += 2;
                
                // Get auth cookie
                authCookie = (int)Util.getDWordBE(tlvData, dcMarker);
                dcMarker +=12;
                
                // Get data for client detection
                dwFT1 = (int) Util.getDWordBE(tlvData, dcMarker);
                dcMarker += 4;
                dwFT2 = (int) Util.getDWordBE(tlvData, dcMarker);
                dcMarker += 4;
                dwFT3 = (int) Util.getDWordBE(tlvData, dcMarker);
                
            } else if (tlvType == 0x0003) {// Signon time
                signon = Util.byteArrayToLong(tlvData); // GMT
            } else if (tlvType == 0x0004) {// Idle time
                idle   = (int)Util.byteArrayToLong(tlvData) / 256;
            } else if (tlvType == 0x000F) {// Online time
                online = (int)Util.byteArrayToLong(tlvData);
                
                // Icon service... and new style status message
            } else if (tlvType == 0x001D) {
                int iconMarker = 0;
                final int BART_STATUS_STR = 0x0002;
                final int BART_STATUS_ID  = 0x000E;
                while (iconMarker < tlvData.length) {
                    int bartType = Util.getWordBE(tlvData, iconMarker);
                    int recordLen = Util.getByte(tlvData, iconMarker + 3);
                
                    if (0 == recordLen) {
                        
                    } else if (BART_STATUS_ID == bartType) {
                        mood = StringConvertor.utf8beByteArrayToString(tlvData, iconMarker + 4, recordLen);
                        
                    } else if (BART_STATUS_STR == bartType) {
                        int len = Util.getWordBE(tlvData, iconMarker + 4);
                        int pos = iconMarker + 6;
                        statusText = StringConvertor.utf8beByteArrayToString(tlvData, pos, len);
                    }
                    iconMarker += 2 + 1 + 1 + recordLen;
                }
            }
            marker += 2 + 2 + tlvData.length;
        }
        // #sijapp cond.if modules_DEBUGLOG is "true" #
        DebugLog.println("mood for " + uin + " " + mood);
        // #sijapp cond.end #
        IcqContact contact = (IcqContact)getIcq().getItemByUIN(uin);
        if (contact != null) {
            // TODO: happy flag
            int flags = (status >> 16) & 0xFFFF;

            // #sijapp cond.if modules_XSTATUSES is "true" #
            contact.setXStatus(Icq.xstatus.createXStatus(capabilities_old, mood), null);
            // #sijapp cond.end #
            statusText = StringConvertor.trim(statusText);
            if (!StringConvertor.isEmpty(statusText)) {
                // #sijapp cond.if modules_XSTATUSES is "true" #
                if (XStatusInfo.XSTATUS_NONE != contact.getXStatusIndex()) {
                    contact.setXStatusMessage(statusText);
                    statusText = null;
                }
                // #sijapp cond.end #
            }
            getIcq().setContactStatus(contact,
                    IcqStatusInfo.getStatusIndex(status & 0xFFFF, capabilities_old),
                    statusText);
            
            
            Client client = contact.getClient();
            if (Client.noneClient == client) {
                client = new Client();
                contact.setClient(client);
            }
            client.reset();

            contact.happyFlag = ((flags & FLAG_HAPPY) != 0)
                    || GUID.CAP_QIP_HAPPY.equals(capabilities_old);

            byte[] capa = mergeCapabilities(capabilities_old, capabilities_new);
            capabilities_old = null;
            capabilities_new = null;
            client.setCapabilities(capa, icqProt);

            // #sijapp cond.if modules_CLIENTS is "true" #
            // Update time values
            long now = Util.createCurrentDate(false);
            contact.setTimeOfChaingingStatus(signon);
            ClientDetector.instance.execVM(client, capa, new int[] {dwFT1, dwFT2, dwFT3}, icqProt);
            // #sijapp cond.end #
            
            // Update contact list
            getIcq().ui_changeContactStatus(contact);
            
            // #sijapp cond.if modules_DEBUGLOG is "true" #
        } else {
            DebugLog.println("USER_ONLINE for " + uin + " (0x" + Integer.toHexString(status) + ")");
            // #sijapp cond.end#
        }
    }

    // #sijapp cond.if modules_XSTATUSES is "true" #
    private XtrazMessagePlugin unpackPlugin(byte[] buf, int pos, String uin) {
        // size + XTRAZ_GUID + func
        int dataPos = pos + 2 + Util.getWordLE(buf, pos);
        pos += 2;
        int guidPos = pos;
        pos += 16;
        int function = Util.getWordLE(buf, pos);
        pos += 2;
        return guidToPlugin(buf, guidPos, dataPos, function, uin);
    }
    private XtrazMessagePlugin guidToPlugin(byte[] buf, int guidPos, int bufPos, int function, String uin) {
        if (XtrazMessagePlugin.XTRAZ_GUID.equals(buf, guidPos, 16)) {
            return parseXtrazMessage(uin, buf, bufPos);
        }
        return null;
    }

    private String getText(String text) {
        return Util.xmlUnescape(text);
    }
    
    private String getTagContent(String xml, String tag) {
        int begin = xml.indexOf("<" + tag + ">");
        int end   = xml.indexOf("</" + tag + ">");
        if (begin >= 0 && end > begin) {
            int offset = tag.length() + 2;
            return getText(xml.substring(begin + offset, end));
        }
        return "";
    }
    private String makeXPromt(String promt) {
        return Util.xmlEscape(promt);
    }

    private static String[] tags = Util.explode("title|desc", '|');
    private XtrazMessagePlugin parseXtrazMessage(String uin, byte[] buf, int pos) {
        Icq icq = getIcq();
        IcqContact contact = (IcqContact)icq.getItemByUIN(uin);
        pos += 4;
        long xmlLen = Math.min(Util.getDWordLE(buf, pos), buf.length - pos - 4);
        String xml = StringConvertor.utf8beByteArrayToString(buf, pos + 4, (int)xmlLen);

        // #sijapp cond.if modules_MAGIC_EYE is "true" #
        if (xml.startsWith("<N>")) {
            MagicEye.addAction(icq, uin, "read xtraz");
        }
        // #sijapp cond.end #

        if (contact == null) return null;

        if (xml.startsWith("<NR>")) {
            String res = getTagContent(xml, "RES");
            String title = StringConvertor.notNull(getTagContent(res, tags[0])).trim();
            String desc = StringConvertor.notNull(getTagContent(res, tags[1])).trim();
            String text = (title + ' ' + desc).trim();
            contact.setXStatusMessage(text);
            getIcq().updateStatusView(contact);
            
        } else if (xml.startsWith("<N>")) {
            if (!icq.isMeVisible(contact)) {
                return null;
            }
            int id = icq.getProfile().xstatusIndex;
            if (id < 0) {
                return null;
            }
            
            String title = icq.getProfile().xstatusTitle;
            String desc  = icq.getProfile().xstatusDescription;
            String str ="<ret event='OnRemoteNotification'><srv>"
                    + "<id>cAwaySrv</id><val srv_id='cAwaySrv'>"
                    + "<Root>"
                    + "<CASXtraSetAwayMessage></CASXtraSetAwayMessage>"
                    + "<uin>" + icq.getUserId() + "</uin>"
                    + "<index>" + id + "</index>"
                    + "<title>" + makeXPromt(title) + "</title>"
                    + "<desc>"  + makeXPromt(desc) + "</desc>"
                    + "</Root></val></srv></ret>";
            str = Util.replace(makeXPromt(str), "&apos;", "'");
            return new XtrazMessagePlugin(contact, "<NR><RES>" + str + "</RES></NR>");
        }
        return null;
    }
    // #sijapp cond.end #
}
// #sijapp cond.end #
